////////////////////////////////////////////////////////////////////////////////
///
/// SoundStretch main routine.
///
/// Author        : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// Last changed  : $Date: 2014-01-07 21:41:23 +0200 (Tue, 07 Jan 2014) $
// File revision : $Revision: 4 $
//
// $Id: main.cpp 187 2014-01-07 19:41:23Z oparviai $
//
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
//  SoundTouch audio processing library
//  Copyright (c) Olli Parviainen
//
//  This library is free software; you can redistribute it and/or
//  modify it under the terms of the GNU Lesser General Public
//  License as published by the Free Software Foundation; either
//  version 2.1 of the License, or (at your option) any later version.
//
//  This library is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
//  Lesser General Public License for more details.
//
//  You should have received a copy of the GNU Lesser General Public
//  License along with this library; if not, write to the Free Software
//  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
////////////////////////////////////////////////////////////////////////////////

#include <stdexcept>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include "RunParameters.h"
#include "WavFile.h"
#include "SoundTouch.h"
#include "BPMDetect.h"

using namespace soundtouch;
using namespace std;

// Processing chunk size (size chosen to be divisible by 2, 4, 6, 8, 10, 12, 14, 16 channels ...)
#define BUFF_SIZE           6720

#ifdef _WIN32
    #include <io.h>
    #include <fcntl.h>

    // Macro for Win32 standard input/output stream support: Sets a file stream into binary mode
    #define SET_STREAM_TO_BIN_MODE(f) (_setmode(_fileno(f), _O_BINARY))
#else
    // Not needed for GNU environment... 
    #define SET_STREAM_TO_BIN_MODE(f) {}
#endif

/*
static const char _helloText[] = 
    "\n"
    "   SoundStretch v%s -  Written by Olli Parviainen 2001 - 2014\n"
    "==================================================================\n"
    "author e-mail: <oparviai"
    "@"
    "iki.fi> - WWW: http://www.surina.net/soundtouch\n"
    "\n"
    "This program is subject to (L)GPL license. Run \"soundstretch -license\" for\n"
    "more information.\n"
    "\n";
//*/
static void openFiles(WavInFile **inFile, WavOutFile **outFile, const RunParameters *params, const unsigned char *pbData, int nData)
{
    int bits, samplerate, channels;
    WavInFile *fIn = NULL;
    WavOutFile *fOut = NULL;
    // to clear output
    *inFile = NULL;
    *outFile = NULL;
    // open input file...
    if (pbData!=NULL && nData>0)
        fIn = new WavInFile(pbData, nData);
    else if (params->inFileName!=NULL && *params->inFileName)
        fIn = new WavInFile(params->inFileName);
    // ... open output file with same sound parameters
    bits = (int)fIn->getNumBits();
    samplerate = (int)fIn->getSampleRate();
    channels = (int)fIn->getNumChannels();

    if (params->outFileName) {
        fOut = new WavOutFile(params->outFileName);
        fOut->initWave(samplerate, bits, channels);
    }
    // to output
    *inFile = fIn;
    *outFile = fOut;
}

// Sets the 'SoundTouch' object up according to input file sound format & 
// command line parameters
static void setup(SoundTouch *pSoundTouch, const WavInFile *inFile, const RunParameters *params)
{
    int sampleRate;
    int channels;

    sampleRate = (int)inFile->getSampleRate();
    channels = (int)inFile->getNumChannels();
    pSoundTouch->setSampleRate(sampleRate);
    pSoundTouch->setChannels(channels);

    pSoundTouch->setTempoChange(params->tempoDelta);
    pSoundTouch->setPitchSemiTones(params->pitchDelta);
    pSoundTouch->setRateChange(params->rateDelta);

    pSoundTouch->setSetting(SETTING_USE_QUICKSEEK, params->quick);
    pSoundTouch->setSetting(SETTING_USE_AA_FILTER, !(params->noAntiAlias));

    if (params->speech)
    {
        // use settings for speech processing
        pSoundTouch->setSetting(SETTING_SEQUENCE_MS, 40);
        pSoundTouch->setSetting(SETTING_SEEKWINDOW_MS, 15);
        pSoundTouch->setSetting(SETTING_OVERLAP_MS, 8);
        //fprintf(stderr, "Tune processing parameters for speech processing.\n");
    }

    // print processing information
    if (params->outFileName)
    {
#ifdef SOUNDTOUCH_INTEGER_SAMPLES
        //fprintf(stderr, "Uses 16bit integer sample type in processing.\n\n");
#else
    #ifndef SOUNDTOUCH_FLOAT_SAMPLES
        #error "Sampletype not defined"
    #endif
        //fprintf(stderr, "Uses 32bit floating point sample type in processing.\n\n");
#endif
        // print processing information only if outFileName given i.e. some processing will happen
        //fprintf(stderr, "Processing the file with the following changes:\n");
        //fprintf(stderr, "  tempo change = %+g %%\n", params->tempoDelta);
        //fprintf(stderr, "  pitch change = %+g semitones\n", params->pitchDelta);
        //fprintf(stderr, "  rate change  = %+g %%\n\n", params->rateDelta);
        //fprintf(stderr, "Working...");
    }
    else
    {
        // outFileName not given
        //fprintf(stderr, "Warning: output file name missing, won't output anything.\n\n");
    }

    //fflush(stderr);
}

// Processes the sound
static void process(SoundTouch *pSoundTouch, WavInFile *inFile, IWaveOut *pOut)
{
    int nSamples;
    int nChannels;
    int buffSizeSamples;
    SAMPLETYPE sampleBuffer[BUFF_SIZE];

    if ((inFile == NULL) || (pOut == NULL))
        return;  // nothing to do.

    nChannels = (int)inFile->getNumChannels();
    assert(nChannels > 0);
    buffSizeSamples = BUFF_SIZE / nChannels;

    // Process samples read from the input file
    while (inFile->eof() == 0) {
        int num;
        // Read a chunk of samples from the input file
        num = inFile->read(sampleBuffer, BUFF_SIZE);
        nSamples = num / (int)inFile->getNumChannels();
        // Feed the samples into SoundTouch processor
        pSoundTouch->putSamples(sampleBuffer, nSamples);
        // Read ready samples from SoundTouch processor & write them output file.
        // NOTES:
        // - 'receiveSamples' doesn't necessarily return any samples at all
        //   during some rounds!
        // - On the other hand, during some round 'receiveSamples' may have more
        //   ready samples than would fit into 'sampleBuffer', and for this reason 
        //   the 'receiveSamples' call is iterated for as many times as it
        //   outputs samples.
        do {
            nSamples = pSoundTouch->receiveSamples(sampleBuffer, buffSizeSamples);
            if (nSamples <= 0)
                break;
            pOut->write(sampleBuffer, nSamples * nChannels);
        } while (nSamples != 0);
    }
    // Now the input file is processed, yet 'flush' few last samples that are
    // hiding in the SoundTouch's internal processing pipeline.
    pSoundTouch->flush();
    do {
        nSamples = pSoundTouch->receiveSamples(sampleBuffer, buffSizeSamples);
        if (nSamples <= 0)
            break;
        pOut->write(sampleBuffer, nSamples * nChannels);
    } while (nSamples != 0);
    pOut->endStream();
}

// Detect BPM rate of inFile and adjust tempo setting accordingly if necessary
static void detectBPM(WavInFile *inFile, RunParameters *params)
{
    float bpmValue;
    int nChannels;
    BPMDetect bpm(inFile->getNumChannels(), inFile->getSampleRate());
    SAMPLETYPE sampleBuffer[BUFF_SIZE];

    // detect bpm rate
    //fprintf(stderr, "Detecting BPM rate...");
    //fflush(stderr);

    nChannels = (int)inFile->getNumChannels();
    assert(BUFF_SIZE % nChannels == 0);

    // Process the 'inFile' in small blocks, repeat until whole file has 
    // been processed
    while (inFile->eof() == 0)
    {
        int num, samples;

        // Read sample data from input file
        num = inFile->read(sampleBuffer, BUFF_SIZE);

        // Enter the new samples to the bpm analyzer class
        samples = num / nChannels;
        bpm.inputSamples(sampleBuffer, samples);
    }

    // Now the whole song data has been analyzed. Read the resulting bpm.
    bpmValue = bpm.getBpm();
    //fprintf(stderr, "Done!\n");

    // rewind the file after bpm detection
    inFile->rewind();

    if (bpmValue > 0)
    {
        //fprintf(stderr, "Detected BPM rate %.1f\n\n", bpmValue);
    }
    else
    {
        //fprintf(stderr, "Couldn't detect BPM rate.\n\n");
        return;
    }

    if (params->goalBPM > 0)
    {
        // adjust tempo to given bpm
        params->tempoDelta = (params->goalBPM / bpmValue - 1.0f) * 100.0f;
        //fprintf(stderr, "The file will be converted to %.1f BPM\n\n", params->goalBPM);
    }
}

typedef const char *LPCSTR;
extern "C"
int soundstretch_main(int nParams, const LPCSTR paramStr[])
{
    WavInFile *inFile;
    WavOutFile *outFile;
    SoundTouch soundTouch;

    try
    {
        // Parse command line parameters
        RunParameters params(nParams, paramStr);
        // Open input & output files
        openFiles(&inFile, &outFile, &params, NULL, 0);
        if (params.detectBPM != 0)
        {
            // detect sound BPM (and adjust processing parameters accordingly if necessary)
            detectBPM(inFile, &params);
        }
        // Setup the 'SoundTouch' object for processing the sound
        setup(&soundTouch, inFile, &params);
        // Process the sound
        process(&soundTouch, inFile, outFile);
        // Close WAV file handles & dispose of the objects
        delete inFile;
        delete outFile;
    }
    catch (const runtime_error &e) 
    {
        // An exception occurred during processing, display an error message
        //fprintf(stderr, "%s\n", e.what());
        return -1;
    }

    return 0;
}

#define FLAG_MUSIC      0x00
#define FLAG_SPEECH     0x01
#define FLAG_QUICK      0x02
#define FLAG_DETECTBPM  0x04
#define FLAG_NOANTIALIAS    0x08

extern "C"
int soundstretch_proc(LPCSTR szFileIn, LPCSTR szFileOut, const float *delta3, int nOption)
{
    WavInFile *inFile;
    WavOutFile *outFile;
    SoundTouch soundTouch;
    
    try
    {
        // Parse command line parameters
        RunParameters params;
        params.inFileName = szFileIn;
        params.outFileName = szFileOut;
        params.tempoDelta = delta3[0];
        params.pitchDelta = delta3[1];
        params.rateDelta = delta3[2];
        params.speech = ((nOption & FLAG_SPEECH) != 0);
        params.quick = ((nOption & FLAG_QUICK) != 0);
        params.detectBPM = ((nOption & FLAG_DETECTBPM) != 0);
        params.noAntiAlias = ((nOption & FLAG_NOANTIALIAS) != 0);
        // Open input & output files
        openFiles(&inFile, &outFile, &params, NULL, 0);
        if (params.detectBPM != 0)
        {
            // detect sound BPM (and adjust processing parameters accordingly if necessary)
            detectBPM(inFile, &params);
        }
        // Setup the 'SoundTouch' object for processing the sound
        setup(&soundTouch, inFile, &params);
        // Process the sound
        process(&soundTouch, inFile, outFile);
        // Close WAV file handles & dispose of the objects
        delete inFile;
        delete outFile;
    }
    catch (const runtime_error &e)
    {
        // An exception occurred during processing, display an error message
        //fprintf(stderr, "%s\n", e.what());
        return -1;
    }
    
    return 0;
}

extern "C"
int soundstretch_fromData(const unsigned char *pbData, int nData, LPCSTR szFileOut, const float *delta3, int nOption)
{
    WavInFile *inFile;
    WavOutFile *outFile;
    SoundTouch soundTouch;
    
    try
    {
        // Parse command line parameters
        RunParameters params;
        params.inFileName = NULL;
        params.outFileName = szFileOut;
        params.tempoDelta = delta3[0];
        params.pitchDelta = delta3[1];
        params.rateDelta = delta3[2];
        params.speech = ((nOption & FLAG_SPEECH) != 0);
        params.quick = ((nOption & FLAG_QUICK) != 0);
        params.detectBPM = ((nOption & FLAG_DETECTBPM) != 0);
        params.noAntiAlias = ((nOption & FLAG_NOANTIALIAS) != 0);
        // Open input & output files
        openFiles(&inFile, &outFile, &params, pbData, nData);
        if (params.detectBPM != 0)
        {
            // detect sound BPM (and adjust processing parameters accordingly if necessary)
            detectBPM(inFile, &params);
        }
        // Setup the 'SoundTouch' object for processing the sound
        setup(&soundTouch, inFile, &params);
        // Process the sound
        process(&soundTouch, inFile, outFile);
        // Close WAV file handles & dispose of the objects
        delete inFile;
        delete outFile;
    }
    catch (const runtime_error &e)
    {
        // An exception occurred during processing, display an error message
        //fprintf(stderr, "%s\n", e.what());
        return -1;
    }
    
    return 0;
}

typedef void (*LPWaveCallback)(void *lpContext, int nCase, void *lpData, int nBytes);

class CWaveOutAgent : public IWaveOut
{
public:
    CWaveOutAgent(LPWaveCallback lpfnCall, void *lpContext) {
        m_fnCall = lpfnCall;
        m_lpContext = lpContext;
        m_pBuf = NULL;
        m_nBuf = 0;
        m_nSampleRate = 8000;
        m_nBits = 16;
    }
    ~CWaveOutAgent() {
        m_fnCall = NULL;
        m_lpContext = NULL;
        if (m_pBuf != NULL) {
            delete[] m_pBuf;
            m_pBuf = NULL;
        }
    }
    
public:
    virtual int initWave(int nSampleRate, int bits, int channel)
    {
        m_nSampleRate = nSampleRate;
        m_nBits = bits;
        int nData[] = {nSampleRate, bits, channel, sizeof(OUTTYPE)};
        m_fnCall(m_lpContext, 0, nData, sizeof(int)*4);
        return 0;
    }
    virtual void write(const OUTTYPE *buffer, int numElems)
    {
        if (m_nSampleRate<=0 || sizeof(OUTTYPE)!=sizeof(float) || (m_nBits!=16 && m_nBits!=8))
            return;
        // old buffer may be too small
        if (m_pBuf && m_nBuf<numElems) {
            delete[] m_pBuf;
            m_pBuf = NULL;
        }
        // to prepare buffer
        if (!m_pBuf) {
            int nAlloc = max(numElems, m_nSampleRate);
            m_pBuf = new short[nAlloc];
            m_nBuf = nAlloc;
        }
        // to support 16-bits and 8-bits
        if (m_nBits == 16) {
            for (int i = 0; i < numElems; i ++) {
                float f = buffer[i] * 32768.0f;
                int n = (int)(f>=0 ? (int)(f+0.5f) : (int)(f-0.5f));
                m_pBuf[i] = (short)(n>0 ? min(n, 32767) : max(n, -32768));
            }
        } else {
            unsigned char *pb = (unsigned char *)m_pBuf;
            for (int i = 0; i < numElems; i ++) {
                float f = buffer[i] * 256.0f;
                int n = (int)(f>=0 ? (int)(f+0.5f) : (int)(f-0.5f));
                pb[i] = (unsigned char)(n>0 ? min(n, 127) : max(n, -128));
            }
        }
        // to call callback function
        m_fnCall(m_lpContext, 1, (void *)m_pBuf, m_nBits/8*numElems);
    }
    virtual void endStream(void)
    {
        m_fnCall(m_lpContext, 2, NULL, 0);
    }
    
private:
    // callback function
    LPWaveCallback m_fnCall;
    // context of callback function
    void    *m_lpContext;
    
private:
    // buffer for pcm data
    short   *m_pBuf;
    // count of m_pBuf
    int     m_nBuf;
    // sample rate, such as 8000, 11025, 44100, 48000
    int     m_nSampleRate;
    // bits, should be 16 or 8, others not supported now
    int     m_nBits;
};

extern "C"
int createStretchedWave(LPCSTR szWaveInFile, LPWaveCallback lpfnCallback, void *lpContext, float fDelta[3], int nOption)
{
    RunParameters params;
    SoundTouch soundTouch;

    if (!szWaveInFile || !*szWaveInFile || !lpfnCallback)
        return -1;
    try {
        WavInFile fIn(szWaveInFile);
        CWaveOutAgent fOut(lpfnCallback, lpContext);
        // Open input & output files
        int bits = (int)fIn.getNumBits();
        int samplerate = (int)fIn.getSampleRate();
        int channels = (int)fIn.getNumChannels();
        fOut.initWave(samplerate, bits, channels);
        // Setup the 'SoundTouch' object for processing the sound
        params.tempoDelta = fDelta[0];
        params.pitchDelta = fDelta[1];
        params.rateDelta = fDelta[2];
        params.speech = ((nOption & FLAG_SPEECH) != 0);
        params.detectBPM = ((nOption & FLAG_DETECTBPM) != 0);
        params.quick = ((nOption & FLAG_QUICK) != 0);
        params.noAntiAlias = ((nOption & FLAG_NOANTIALIAS) != 0);
        setup(&soundTouch, &fIn, &params);
        // Process the sound
        process(&soundTouch, &fIn, &fOut);
    } catch (const runtime_error &e) {
        // An exception occurred during processing, display an error message
        return -1;
    }
    return 0;
}

extern "C"
int createStretchedWave_fromData(const unsigned char *pbData, int nData, LPWaveCallback lpfnCallback, void *lpContext, float fDelta[3], int nOption)
{
    RunParameters params;
    SoundTouch soundTouch;
    
    if (!pbData || nData<=0 || !lpfnCallback)
        return -1;
    try {
        WavInFile fIn(pbData, nData);
        CWaveOutAgent fOut(lpfnCallback, lpContext);
        // Open input & output files
        int bits = (int)fIn.getNumBits();
        int samplerate = (int)fIn.getSampleRate();
        int channels = (int)fIn.getNumChannels();
        fOut.initWave(samplerate, bits, channels);
        // Setup the 'SoundTouch' object for processing the sound
        params.tempoDelta = fDelta[0];
        params.pitchDelta = fDelta[1];
        params.rateDelta = fDelta[2];
        params.speech = ((nOption & FLAG_SPEECH) != 0);
        params.detectBPM = ((nOption & FLAG_DETECTBPM) != 0);
        params.quick = ((nOption & FLAG_QUICK) != 0);
        params.noAntiAlias = ((nOption & FLAG_NOANTIALIAS) != 0);
        setup(&soundTouch, &fIn, &params);
        // Process the sound
        process(&soundTouch, &fIn, &fOut);
    } catch (const runtime_error &e) {
        // An exception occurred during processing, display an error message
        return -1;
    }
    return 0;
}
